package com.atguigu.gmall.activity.service.impl;

import com.atguigu.gmall.activity.model.OrderRecode;
import com.atguigu.gmall.activity.model.SeckillGoods;
import com.atguigu.gmall.activity.model.UserRecode;
import com.atguigu.gmall.activity.service.SeckillBizService;
import com.atguigu.gmall.activity.service.SeckillGoodsService;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.DateUtil;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.common.util.SnowFlake;
import com.atguigu.gmall.order.client.OrderFeignClient;
import com.atguigu.gmall.order.model.OrderDetail;
import com.atguigu.gmall.order.model.OrderInfo;
import com.atguigu.gmall.rabbit.config.MqConst;
import com.atguigu.gmall.rabbit.service.RabbitService;
import com.atguigu.gmall.user.client.UserFeignClient;
import com.atguigu.gmall.user.model.UserAddress;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Handler;

/**
 * @author: atguigu
 * @create: 2023-08-16 10:55
 */
@Slf4j
@Service
public class SeckillBizServiceImpl implements SeckillBizService {

    @Autowired
    private Cache<String, String> seckillCache;

    @Autowired
    private SeckillGoodsService seckillGoodsService;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SnowFlake snowFlake;

    @Autowired
    private UserFeignClient userFeignClient;


    @Autowired
    private OrderFeignClient orderFeignClient;

    /**
     * 生成用户秒杀商品抢购码
     *
     * @param userId
     * @param skuId
     * @return
     */
    @Override
    public String getSeckillBuyCode(String userId, Long skuId) {
        //1.判断本地缓存中商品状态 必须1代表正在秒杀 0：代表售罄  2：秒杀结束
        String status = seckillCache.getIfPresent(skuId.toString());
        if ("2".equals(status)) {
            throw new RuntimeException("秒杀已结束！");
        } else if ("0".equals(status)) {
            throw new RuntimeException("商品已售罄！");
        } else {
            //状态等于1
            //2.判断秒杀商品是否在销售时间内
            SeckillGoods seckillGoods = seckillGoodsService.getSecGoodsById(skuId);
            if (seckillGoods != null) {
                Date now = new Date();  //当前时间大于开始时间 并且 小于结束时间
                Date startTime = seckillGoods.getStartTime();
                Date endTime = seckillGoods.getEndTime();
                if (now.getTime() >= startTime.getTime() && now.getTime() <= endTime.getTime()) {
                    //3.生成抢购码 保证发放验证码，再次验证是否正确 todo 将抢购码放入Redis 不放也行md5加密效率很高
                    String buyCode = MD5.encrypt(userId + skuId);
                    return buyCode;
                }
            }
        }
        throw new RuntimeException("请求非法！");
    }


    /**
     * 秒杀请求入队（将秒杀意向发送MQ就结束）
     *
     * @param userId
     * @param skuId
     * @param buyCode
     */
    @Override
    public void seckillOrderToQueue(String userId, Long skuId, String buyCode) {
        //1.验证用户提交抢购码是否正确
        //1.1 按照相同方式对用户ID+商品ID进行MD5加密
        String encrypt = MD5.encrypt(userId + skuId);
        //1.2 跟用户提交抢购码进行判断
        if (!buyCode.equals(encrypt)) {
            throw new RuntimeException("抢购码验证失败，请重新下单！");
        }

        //2.验证秒杀商品在本地缓存中状态
        String status = seckillCache.getIfPresent(skuId.toString());
        if ("2".equals(status)) {
            throw new RuntimeException("抢购活动已结束！");
        }
        if ("0".equals(status)) {
            throw new RuntimeException("商品已售罄！");
        }
        //3.todo 验证商品秒杀时间
        //4. 避免用户多次排队 采用setnx
        // 如果只允许买秒杀商品中其中一件
        //String reqeueKey = RedisConst.SECKILL_USER + userId + ":requeue";
        // 如果只允许买秒杀商品中的多件
        //todo 时间动态计算 秒杀商品结束时间-当前系统时间
        String reqeueKey = RedisConst.SECKILL_USER + userId + ":" + skuId + ":requeue";
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(reqeueKey, userId, 600, TimeUnit.MINUTES);
        if (!flag) {
            //throw new RuntimeException("请勿重复排队！");
            return;
        }
        //5.将当前秒杀意向商品封装对象(商品ID，用户ID，商品数量) 发送到 消息队列
        if ("1".equals(status)) {
            //5.1 构建用户秒杀商品对象
            UserRecode userRecode = new UserRecode();
            userRecode.setUserId(userId);
            userRecode.setSkuId(skuId);
            userRecode.setNum(1); //从秒杀商品信息中获取
            //5.1 发送秒杀消息到秒杀队列
            rabbitService.sendMsg(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);
        }
    }


    /**
     * 处理用户秒杀请求
     *
     * @param userRecode
     */
    @Override
    public void processSeckillRequest(UserRecode userRecode) {
        //1.验证本地缓存中商品状态是否为1
        String status = seckillCache.getIfPresent(userRecode.getSkuId().toString());
        if (!"1".equals(status)) {
            log.error("[秒杀服务]处理秒杀请求，商品已售罄：{}", userRecode);
            //忽略该业务处理
            return;
        }

        //2.验证用户是否已经下过单
        String orderHashKey = RedisConst.SECKILL_ORDERS_USERS;
        BoundHashOperations<String, String, Long> orderHashOps = redisTemplate.boundHashOps(orderHashKey);
        String hashKey = userRecode.getUserId() + ":" + userRecode.getSkuId();
        if (orderHashOps.hasKey(hashKey)) {
            log.error("[秒杀服务]用户重复下单：{}", userRecode);
            return;
        }
        //3.验证秒杀库存是否充足（避免超卖）从List结构中弹出数据
        String stockListKey = RedisConst.SECKILL_STOCK_PREFIX + userRecode.getSkuId();
        BoundListOperations<String, String> stockListOps = redisTemplate.boundListOps(stockListKey);
        String hasStock = stockListOps.rightPop();
        if (StringUtils.isBlank(hasStock)) {
            log.error("[秒杀服务]秒杀商品库存为空：{}", userRecode);
            //通知所有秒杀服务节点更新本地缓存状态为：0
            redisTemplate.convertAndSend("seckillpush", userRecode.getSkuId() + ":0");
            return;
        }

        //4.为获取到秒杀资格用户产生临时订单-存入Redis（用户、商品、订单信息）
        //4.1 构建临时订单对象
        OrderRecode orderRecode = new OrderRecode();
        orderRecode.setUserId(userRecode.getUserId());
        orderRecode.setNum(userRecode.getNum());
        orderRecode.setSeckillGoods(seckillGoodsService.getSecGoodsById(userRecode.getSkuId()));
        //临时订单编号保证唯一 SEC+日期+雪花算法(将来用户真正提交秒杀订单编号)
        String today = DateUtil.formatDate(new Date()).replaceAll("-", "");
        String outTradeNo = "SEC" + today + snowFlake.nextId();
        orderRecode.setOrderStr(outTradeNo);
        //4.2 存入Redis
        String tempOrderKey = RedisConst.SECKILL_ORDERS;
        BoundHashOperations<String, String, OrderRecode> tempOrderHashOps = redisTemplate.boundHashOps(tempOrderKey);
        tempOrderHashOps.put(hashKey, orderRecode);
        //4.3 TODO 避免用户获取到资格不下单，不付钱情况 发送延迟消息 延迟5分钟  监听该订单编号是否产生订单以及判断订单支付状态 未支付-》关单，恢复秒杀库存

        //5.发送异步MQ消息，通知秒杀服务扣减Redis、MySQL商品库存
        rabbitService.sendMsg(MqConst.EXCHANGE_DIRECT_SECKILL_STOCK, MqConst.ROUTING_SECKILL_STOCK, userRecode.getSkuId());
    }

    /**
     * 扣减秒杀库存
     * * MySQL中秒杀商品库存数量
     * * Redis中Hash中商品信息库存数量
     *
     * @param skuId
     */
    @Override
    public void processDeductStock(Long skuId) {
        //1.查询当前商品存放在Redis中库存List获取剩余库存数
        String stockListKey = RedisConst.SECKILL_STOCK_PREFIX + skuId;
        BoundListOperations<String, String> stockListOps = redisTemplate.boundListOps(stockListKey);
        Long stockCount = stockListOps.size();
        //2.更新MySQL中秒杀商品表中剩余库存数量
        LambdaUpdateWrapper<SeckillGoods> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(SeckillGoods::getSkuId, skuId);
        updateWrapper.set(SeckillGoods::getStockCount, stockCount);
        seckillGoodsService.update(updateWrapper);

        //3.更新Redis中秒杀商品剩余库存数量 TODO 双写失败 数据一致性问题 将操作Redis失败操作发送MQ-消费者重试
        String seckillKey = RedisConst.SECKILL_GOODS;
        BoundHashOperations<String, String, SeckillGoods> seckillHashOps = redisTemplate.boundHashOps(seckillKey);
        String hashKey = skuId.toString();
        if (seckillHashOps.hasKey(hashKey)) {
            SeckillGoods seckillGoods = seckillHashOps.get(hashKey);
            seckillGoods.setStockCount(stockCount.intValue());
            seckillHashOps.put(hashKey, seckillGoods);
        }
    }

    /**
     * 检查当前用户秒杀商品结果
     *
     * @param userId
     * @param skuId
     * @return
     */
    @Override
    public Result checkSeckillResult(String userId, Long skuId) {
        //当前登录用户对指定商品秒杀结果
        //1.如果用户在排队-有机会获取秒杀资格
        String reqeueKey = RedisConst.SECKILL_USER + userId + ":" + skuId + ":requeue";
        Boolean ifQuque = redisTemplate.hasKey(reqeueKey);
        if (ifQuque) {
            //1.1.判断用户是否产生秒杀订单-响应218
            String orderHashKey = RedisConst.SECKILL_ORDERS_USERS;
            //如果用户提交秒杀订单并支付产生 用户秒杀订单hash  其中hashValue存放秒杀订单ID
            BoundHashOperations<String, String, Long> orderHashOps = redisTemplate.boundHashOps(orderHashKey);
            String hashKey = userId + ":" + skuId;
            if (orderHashOps.hasKey(hashKey)) {
                //用户如果提交了订单，在前端页面：我的订单列表 故业务数据响应订单ID
                return Result.build(orderHashOps.get(hashKey), ResultCodeEnum.SECKILL_ORDER_SUCCESS);
            }
            //1.2.判断用户是否产生临时订单-响应215
            String tempOrderKey = RedisConst.SECKILL_ORDERS;
            BoundHashOperations<String, String, OrderRecode> tempOrderHashOps = redisTemplate.boundHashOps(tempOrderKey);
            if (tempOrderHashOps.hasKey(hashKey)) {
                //说明用户获取到秒杀资格，在前端页面：抢单成功 故响应临时订单信息
                return Result.build(tempOrderHashOps.get(hashKey), ResultCodeEnum.SECKILL_SUCCESS);
            }
            //1.3.没有满足以上两个条件，排队中。响应211
            return Result.build(null, ResultCodeEnum.SECKILL_RUN);
        }
        //2.如果用户未在排队且本地缓存中商品状态为"0"-响应213
        String status = seckillCache.getIfPresent(skuId.toString());
        if (!ifQuque && "0".equals(status)) {
            return Result.build(null, ResultCodeEnum.SECKILL_FINISH);
        }
        return Result.build(null, ResultCodeEnum.SECKILL_RUN);
    }


    /**
     * 汇总秒杀订单确认页面数据汇总
     *
     * @param userId 用户ID
     * @param skuId  秒杀商品ID
     * @return ${userAddressList}:地址列表
     * ${detailArrayList}:订单明细列表
     * ${totalNum}:总数
     * ${totalAmount}:总金额
     */
    @Override
    public Map<String, Object> getSeckillOrderTradeData(String userId, Long skuId) {
        Map<String, Object> mapResult = new HashMap<>();
        //1.远程调用用户服务获取收件地址-todo 异步任务
        List<UserAddress> userAddressList = userFeignClient.getUserAddressListByUserId(Long.valueOf(userId));
        if (!CollectionUtils.isEmpty(userAddressList)) {
            mapResult.put("userAddressList", userAddressList);
        }

        //2.查询秒杀过程中生成临时订单hash-获取秒杀商品信息-得到订单明细
        String tempOrderKey = RedisConst.SECKILL_ORDERS;
        BoundHashOperations<String, String, OrderRecode> tempOrderHashOps = redisTemplate.boundHashOps(tempOrderKey);
        String hashKey = userId + ":" + skuId;
        if (tempOrderHashOps.hasKey(hashKey)) {
            //获取到秒杀商品
            OrderRecode orderRecode = tempOrderHashOps.get(hashKey);
            SeckillGoods seckillGoods = orderRecode.getSeckillGoods();
            //将秒杀商品对象转为订单明细对象
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setSkuId(seckillGoods.getSkuId());
            orderDetail.setImgUrl(seckillGoods.getSkuDefaultImg());
            orderDetail.setOrderPrice(seckillGoods.getCostPrice());
            orderDetail.setSkuName(seckillGoods.getSkuName());
            orderDetail.setSkuNum(orderRecode.getNum());
            List<OrderDetail> orderDetailList = Arrays.asList(orderDetail);
            mapResult.put("detailArrayList", orderDetailList);
            mapResult.put("totalNum", orderRecode.getNum());
            mapResult.put("totalAmount", seckillGoods.getCostPrice());
        }
        return mapResult;
    }

    /**
     * 保存秒杀订单
     *
     * @param orderInfo
     * @return
     */
    @Override
    public Long submitSeckillOrder(OrderInfo orderInfo) {
        //1.远程调用订单微服务保存秒杀订单 注意：查询临时订单，设置秒杀订单编号
        //1.1 查询临时订单获取临时订单信息

        String tempOrderKey = RedisConst.SECKILL_ORDERS;
        BoundHashOperations<String, String, OrderRecode> tempOrderHashOps = redisTemplate.boundHashOps(tempOrderKey);

        List<OrderDetail> orderDetailList = orderInfo.getOrderDetailList();
        if (!CollectionUtils.isEmpty(orderDetailList)) {
            Long skuId = orderDetailList.get(0).getSkuId();
            String hashKey = orderInfo.getUserId() + ":" + skuId;
            OrderRecode orderRecode = tempOrderHashOps.get(hashKey);
            orderInfo.setOutTradeNo(orderRecode.getOrderStr());
            Long orderId = orderFeignClient.submitSeckillOrder(orderInfo);
            //2.删除临时订单hash
            tempOrderHashOps.delete(hashKey);

            //3.新增用户下单记录 hash
            String orderHashKey = RedisConst.SECKILL_ORDERS_USERS;
            BoundHashOperations<String, String, Long> orderHashOps = redisTemplate.boundHashOps(orderHashKey);
            orderHashOps.put(hashKey, orderId);
            return orderId;
        }
        return null;
    }

    /**
     * 清理秒杀缓存
     */
    @Override
    public void processSeckillClean() {
        //1.将分布式缓存中Redis中所有秒杀信息全部删除
        Set<String> keys = redisTemplate.keys("seckill:" + "*");
        redisTemplate.delete(keys);

        //2.将本地缓存中商品状态位全部清理(将所有的秒杀微服务实例（JVM）) TODO 发送发布订阅消息广播所有秒杀节点清理本地缓存（参考以前预热）
        seckillCache.invalidateAll();

        //3.修改秒杀商品表中商品状态
        LambdaUpdateWrapper<SeckillGoods> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(SeckillGoods::getStatus, "0");
        updateWrapper.lt(SeckillGoods::getEndTime, new Date());
        seckillGoodsService.update(updateWrapper);

    }
}
